package com.atguigu.gulimall.seckill.service.impl;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.to.mq.SeckillOrderTo;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberResponseVo;
import com.atguigu.gulimall.seckill.feign.CouponFeignService;
import com.atguigu.gulimall.seckill.feign.ProductFeignService;
import com.atguigu.gulimall.seckill.intercepter.LoginUserInterceptor;
import com.atguigu.gulimall.seckill.service.SeckillService;
import com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo;
import com.atguigu.gulimall.seckill.vo.SeckillSessionsWithSkus;
import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private ProductFeignService productFeignService;

    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private CouponFeignService couponFeignService;


    @Override
    public void uploadSeckillSkuLatest3Days() {
        // 1 去数据库扫描最近三天需要上架的商品
        R r = couponFeignService.getLatest3DaySession();
        if (r.getCode() == 0) {
            // 上架商品
            List<SeckillSessionsWithSkus> sessionData = r.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
            });

            if (sessionData == null || sessionData.size() == 0) {
                return;
            }


            // 缓存到redis
            // 1 缓存活动信息
            saveSessionInfos(sessionData);
            // 2 缓存活动的商品信息
            saveSessionSkuInfos(sessionData);
        }

    }

    /**
     * 熔断方法
     * @param e 出现熔断原因
     * @return 一样的返回值
     */
    public List<SeckillSkuRedisTo> blockHanderSecKillSkus(BlockException e) {
        log.error("seckill 内部方法资源降级。。。" + e.getMessage());
        return null;
    }
    @Override
    @SentinelResource(value = "getCurrentSeckillSkusResource", blockHandler = "blockHanderSecKillSkus")
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        // 1 确定当前时间属于的秒杀场次
        long time = new Date().getTime();
        Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        // "seckill:sessions:1634619600000_1634626800000"
        for (String key : keys) {
            String[] s = key.replace(SESSIONS_CACHE_PREFIX, "").split("_");
            long start = Long.parseLong(s[0]);
            long end = Long.parseLong(s[1]);

            if (time >= start && time <= end) {
                // 2 获取当前场次的商品信息
                List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                if (range == null) {
                    return null;
                }

                BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                List<String> skuList = hashOps.multiGet(range);

                if (skuList != null && skuList.size() > 0) {
                    List<SeckillSkuRedisTo> collect = skuList.stream().map(sku -> {
                        return JSON.parseObject((String) sku, SeckillSkuRedisTo.class);
                    }).collect(Collectors.toList());
                    return collect;
                }

                break;
            }
        }


        return null;
    }

    @Override
    public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if (keys != null && keys.size() > 0) {
            // 3_4
            String regx = "\\d_" + skuId;
            for (String key : keys) {
                if (Pattern.matches(regx, key)) {
                    String json = hashOps.get(key);
                    SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);

                    long current = new Date().getTime();
                    if (redisTo == null) {
                        return null;
                    }

                    // 如果商品秒杀时间已过期
                    if (current > redisTo.getEndTime()) {
                        continue;
                    }

                    // 处理随机码
                    if (current < redisTo.getStartTime()) {
                        redisTo.setRandomCode(null);
                    }

                    return redisTo;
                }
            }
        }

        return null;
    }

    @Override
    public String kill(String killId, String key, Integer num) {
        MemberResponseVo respVo = LoginUserInterceptor.loginUser.get();

        // 1 获取挡墙秒杀商品详细信息
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String s = hashOps.get(killId);

        // 校验
        if (StringUtils.isEmpty(s)) {
            return null;
        }

        SeckillSkuRedisTo redisTo = JSON.parseObject(s, SeckillSkuRedisTo.class);

        // 校验时间
        Long end = redisTo.getEndTime();
        Long start = redisTo.getStartTime();
        long time = new Date().getTime();
        if (time < start || time > end) {
            return null;
        }

        // 校验随机码、killId
        String cliKillId = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
        if (!redisTo.getRandomCode().equals(key) || !cliKillId.equals(killId)) {
            return null;
        }

        // 验证数量
        if (num > redisTo.getSeckillLimit()) {
            return null;
        }

        // 验证这个人是否已经买过，幂等性
        // 买过的人就去 redis 占位，（userId_sessionId_skuId）
        String purchaseId = respVo.getId() + "_" + cliKillId;
        Boolean b = redisTemplate.opsForValue().setIfAbsent(purchaseId, num.toString(), end - start, TimeUnit.MILLISECONDS);
        if (b != null && !b) {
            return null;
        }

        // 尝试减少商品数量
        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + redisTo.getRandomCode());

        boolean tryAcquire = semaphore.tryAcquire(num);
        if (tryAcquire) {
            // 秒杀成功
            // 快速下单，发送mq消息
            String timeId = IdWorker.getTimeId();

            SeckillOrderTo orderTo = new SeckillOrderTo();
            orderTo.setOrderSn(timeId);
            orderTo.setMemberId(respVo.getId());
            orderTo.setNum(num);
            orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
            orderTo.setSkuId(redisTo.getSkuId());
            orderTo.setSeckillPrice(redisTo.getSeckillPrice());
            rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", "");

            return timeId;
        }

        return null;
    }


    /**
     * 缓存活动信息
     */
    private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> sessionData) {
        sessionData.forEach(session -> {
            long start = session.getStartTime().getTime();
            long end = session.getEndTime().getTime();
            String key = SESSIONS_CACHE_PREFIX + start + "_" + end;

            Boolean b;
            // 不存在才添加
            if ((b = redisTemplate.hasKey(key)) != null && !b) {
                List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "_" + item.getSkuId().toString()).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key, collect);
            }
        });
    }

    /**
     * 缓存商品信息
     */
    private void saveSessionInfos(List<SeckillSessionsWithSkus> sessionData) {
        if (sessionData == null)
            return ;

        BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);

        sessionData.forEach(session -> {
            session.getRelationSkus().forEach(seckillSkuVO -> {
                String token = null;
                token = UUID.randomUUID().toString().replace("-", "");

                Boolean b;
                // 不存在才添加
                if ((b = ops.hasKey(seckillSkuVO.getPromotionSessionId() + "_" + seckillSkuVO.getSkuId().toString())) != null && !b) {
                    // 换成商品
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();

                    // 1 sku基本信息
                    R skuInfo = productFeignService.getSkuInfo(seckillSkuVO.getSkuId());
                    if (skuInfo.getCode() == 0) {
                        SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                        });
                        redisTo.setSkuInfo(info);
                    }

                    // 2 sku秒杀信息
                    BeanUtils.copyProperties(seckillSkuVO, redisTo);

                    // 3 设置秒杀时间
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime((session.getEndTime().getTime()));

                    // 4 设置随机码 -》 防止被人恶意刷商品（seckill?skuId=123），设置一个随机码，在秒杀开始的时候开放随机码（seckill?skuId=123&key=uuid）
                    redisTo.setRandomCode(token);

                    ops.put(seckillSkuVO.getPromotionSessionId() + "_" + seckillSkuVO.getSkuId().toString(), JSON.toJSONString(redisTo));
                    // 不存在才添加
                    // 5 引入分布式信号量 限流
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    // 商品可秒杀的数量作为信号量
                    semaphore.trySetPermits(Integer.parseInt(seckillSkuVO.getSeckillCount().toString()));
                }
            });
        });
    }
}
